/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.primitives.stdlib;

import fr.x9c.cadmium.kernel.Block;
import fr.x9c.cadmium.kernel.CodeRunner;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.Primitive;
import fr.x9c.cadmium.kernel.PrimitiveProvider;
import fr.x9c.cadmium.kernel.Value;
import fr.x9c.cadmium.util.Misc;

/**
 * Implements all primitives from 'str.c'.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
@PrimitiveProvider
public final class Str {

    /**
     * No instance of this class.
     */
    private Str() {
    } // end empty constructor

    /**
     * Returns the length of a string.
     * @param ctxt context
     * @param str string value
     * @return length of <tt>str</tt>
     */
    @Primitive
    public static Value caml_ml_string_length(final CodeRunner ctxt,
                                              final Value str) {
        return Value.createFromLong(str.asBlock().sizeBytes());
    } // end method 'caml_ml_string_length(CodeRunner, Value)'

    /**
     * Creates a string of a given length.
     * @param ctxt context
     * @param l length
     * @return a string of length <tt>l</tt>, each byte being set to zero
     * @throws Fail.Exception if length is too big
     */
    @Primitive
    public static Value caml_create_string(final CodeRunner ctxt,
                                           final Value l)
        throws Fail.Exception {
        final int size = l.asLong();
        if (size > 4 * Block.MAX_WOSIZE - 1) {
            Fail.invalidArgument("String.create");
        } // end if
        return Value.createFromBlock(Block.createString(size));
    } // end method 'caml_create_string(CodeRunner, Value)'

    /**
     * Returns a character from a string as a long.
     * @param ctxt context
     * @param s string value
     * @param idx index of character to return
     * @return the character of <tt>s</tt> at index <tt>idx</tt>
     * @throws Fail.Exception if index is invalid
     */
    @Primitive
    public static Value caml_string_get(final CodeRunner ctxt,
                                        final Value s,
                                        final Value idx)
        throws Fail.Exception {
        final Block block = s.asBlock();
        final int i = idx.asLong();
        if ((i < 0) || (i >= block.sizeBytes())) {
            Fail.arrayBoundError();
        } // end if
        return Value.createFromLong(block.getUnsignedByte(i));
    } // end method 'caml_string_get(CodeRunner, Value, Value)'

    /**
     * Changes a character in a string.
     * @param ctxt context
     * @param s string value
     * @param idx index of character to change
     * @param v new character value
     * @return <i>unit</i>
     * @throws Fail.Exception if index is invalid
     */
    @Primitive
    public static Value caml_string_set(final CodeRunner ctxt,
                                        final Value s,
                                        final Value idx,
                                        final Value v)
        throws Fail.Exception {
        final Block block = s.asBlock();
        final int i = idx.asLong();
        if ((i < 0) || (i >= block.sizeBytes())) {
            Fail.arrayBoundError();
        } // end if
        block.setUnsignedByte(i, v.asLong() & 0xFF);
        return Value.UNIT;
    } // end method 'caml_string_set(CodeRunner, Value, Value, Value)'

    /**
     * Compares two string values.
     * @param ctxt context
     * @param s1 first string value to compare
     * @param s2 second string value to compare
     * @return {@link fr.x9c.cadmium.kernel.Value#TRUE} if the two values are
     *         equal,
     *         {@link fr.x9c.cadmium.kernel.Value#FALSE} otherwise
     */
    @Primitive
    public static Value caml_string_equal(final CodeRunner ctxt,
                                          final Value s1,
                                          final Value s2) {
        final byte[] b1 = s1.asBlock().getBytes();
        final byte[] b2 = s2.asBlock().getBytes();

        if (b1 == b2) {
            return Value.TRUE;
        } // end if
        final int len = b1.length;
        if (len != b2.length) {
            return Value.FALSE;
        } // end if
        int i = 0;
        while ((i < len) && (b1[i] == b2[i])) {
            i++;
        } // end while
        return i == len ? Value.TRUE : Value.FALSE;
    } // end method 'caml_string_equal(CodeRunner, Value, Value)'

    /**
     * Compares two string values.
     * @param ctxt context
     * @param s1 first string value to compare
     * @param s2 second string value to compare
     * @return {@link fr.x9c.cadmium.kernel.Value#TRUE} if the two values are
     *         different,
     *         {@link fr.x9c.cadmium.kernel.Value#FALSE} otherwise
     */
    @Primitive
    public static Value caml_string_notequal(final CodeRunner ctxt,
                                             final Value s1,
                                             final Value s2) {
        return caml_string_equal(ctxt, s1, s2) == Value.TRUE
            ? Value.FALSE
            : Value.TRUE;
    } // end method 'caml_string_notequal(CodeRunner, Value, Value)'

    /**
     * Compares two string values.
     * @param ctxt context
     * @param s1 first string value to compare
     * @param s2 second string value to compare
     * @return -1 if the first string value is less than the second one <br/>
     *          0 if the two string values are equal <br/>
     *          1 if the first string value is greater than the second one
     */
    @Primitive
    public static Value caml_string_compare(final CodeRunner ctxt,
                                            final Value s1,
                                            final Value s2) {
        return Value.createFromLong(Integer.signum(compare(s1, s2)));
    } // end method 'caml_string_compare(CodeRunner, Value, Value)'

    /**
     * Compares two string values.
     * @param ctxt context
     * @param s1 first string value to compare
     * @param s2 second string value to compare
     * @return {@link fr.x9c.cadmium.kernel.Value#TRUE} if the first value is
     *         less than the second one,
     *         {@link fr.x9c.cadmium.kernel.Value#FALSE} otherwise
     */
    @Primitive
    public static Value caml_string_lessthan(final CodeRunner ctxt,
                                             final Value s1,
                                             final Value s2) {
        return compare(s1, s2) < 0 ? Value.TRUE : Value.FALSE;
    } // end method 'caml_string_lessthan(CodeRunner, Value, Value)'

    /**
     * Compares two string values.
     * @param ctxt context
     * @param s1 first string value to compare
     * @param s2 second string value to compare
     * @return {@link fr.x9c.cadmium.kernel.Value#TRUE} if the first value is
     *         less than or equal to the second one,
     *         {@link fr.x9c.cadmium.kernel.Value#FALSE} otherwise
     */
    @Primitive
    public static Value caml_string_lessequal(final CodeRunner ctxt,
                                              final Value s1,
                                              final Value s2) {
        return compare(s1, s2) <= 0 ? Value.TRUE : Value.FALSE;
    } // end method 'caml_string_lessequal(CodeRunner, Value, Value)'

    /**
     * Compares two string values.
     * @param ctxt context
     * @param s1 first string value to compare
     * @param s2 second string value to compare
     * @return {@link fr.x9c.cadmium.kernel.Value#TRUE} if the first value is
     *         greater than the second one,
     *         {@link fr.x9c.cadmium.kernel.Value#FALSE} otherwise
     */
    @Primitive
    public static Value caml_string_greaterthan(final CodeRunner ctxt,
                                                final Value s1,
                                                final Value s2) {
        return compare(s1, s2) > 0 ? Value.TRUE : Value.FALSE;
    } // end method 'caml_string_greaterthan(CodeRunner, Value, Value)'

    /**
     * Compares two string values.
     * @param ctxt context
     * @param s1 first string value to compare
     * @param s2 second string value to compare
     * @return {@link fr.x9c.cadmium.kernel.Value#TRUE} if the first value is
     *         greater than or equal to the second one,
     *         {@link fr.x9c.cadmium.kernel.Value#FALSE} otherwise
     */
    @Primitive
    public static Value caml_string_greaterequal(final CodeRunner ctxt,
                                                 final Value s1,
                                                 final Value s2) {
        return compare(s1, s2) >= 0 ? Value.TRUE : Value.FALSE;
    } // end method 'caml_string_greaterequal(CodeRunner, Value, Value)'

    /**
     * Copies characters from a string into another one.
     * @param ctxt context
     * @param s1 first string value
     * @param ofs1 source offset of first character to copy (in <tt>s1</tt>)
     * @param s2 second string value
     * @param ofs2 destination offset of first character to be remplaced
     *        (in <tt>s2</tt>)
     * @param n number of characters to copy
     * @return <i>unit</i>
     */
    @Primitive
    public static Value caml_blit_string(final CodeRunner ctxt,
                                         final Value s1,
                                         final Value ofs1,
                                         final Value s2,
                                         final Value ofs2,
                                         final Value n) {
        System.arraycopy(s1.asBlock().getBytes(),
                         ofs1.asLong(),
                         s2.asBlock().getBytes(),
                         ofs2.asLong(),
                         n.asLong());
        return Value.UNIT;
    } // end 'method caml_blit_string(CodeRunner, Value, Value, Value, Value, Value)'

    /**
     * Fills a portion of a string with a given value.
     * @param ctxt context
     * @param s string to fill
     * @param ofs offset of portion to fill
     * @param len length of portion to fill
     * @param init character to fill string with
     * @return <i>unit</i>
     */
    @Primitive
    public static Value caml_fill_string(final CodeRunner ctxt,
                                         final Value s,
                                         final Value ofs,
                                         final Value len,
                                         final Value init) {
        final int o = ofs.asLong();
        final int l = o + len.asLong();
        final byte val = (byte) Misc.unsignedToSignedByte(init.asLong() & 0xFF);
        final byte[] b = s.asBlock().getBytes();
        for (int i = o; i < l; i++) {
            b[i] = val;
        } // end for
        return Value.UNIT;
    } // end method 'caml_fill_string(CodeRunner, Value, Value, Value, Value)'

    /**
     * Tests if a character is printable.
     * @param ctxt context
     * @param c character to test
     * @return {@link fr.x9c.cadmium.kernel.Value#TRUE} if <tt>c</tt> is
     *         printable,
     *         {@link fr.x9c.cadmium.kernel.Value#FALSE} otherwise
     */
    @Primitive
    public static Value caml_is_printable(final CodeRunner ctxt,
                                          final Value c) {
        final int ch = c.asLong() & 0xFF;
        return (ch >= 0x20) && (ch <= 0x7E) ? Value.TRUE : Value.FALSE;
    } // end method 'caml_is_printable(CodeRunner, Value)'

    /**
     * Tests a bit in a string viewed as a bit vector.
     * @param ctxt context
     * @param b string value / bit vector
     * @param n position of bit to test
     * @return {@link fr.x9c.cadmium.kernel.Value#TRUE} if bit is set,
     *         {@link fr.x9c.cadmium.kernel.Value#FALSE} otherwise
     */
    @Primitive
    public static Value caml_bitvect_test(final CodeRunner ctxt,
                                          final Value b,
                                          final Value n) {
        final int pos = n.asLong();
        return (b.asBlock().getUnsignedByte(pos >> 3) & (1 << (pos & 7))) != 0
            ? Value.TRUE
            : Value.FALSE;
    } // end method 'caml_bitvect_test(CodeRunner, Value, Value)'

    /**
     * Compares the two passed strings as <i>memcmp</i> would do.
     * @param s1 first string to compare
     * @param s2 first string to compare
     * @return an integer following the OCaml <i>compare</i> convention
     */
    private static final int compare(final Value s1, final Value s2) {
        final byte[] b1 = s1.asBlock().getBytes();
        final byte[] b2 = s2.asBlock().getBytes();

        if (b1 == b2) {
            return 0;
        } // end if
        final int len1 = b1.length;
        final int len2 = b2.length;
        final int len = Math.min(len1, len2);
        int i = 0;
        while ((i < len) && (b1[i] == b2[i])) {
            i++;
        } // end while
        if (i < len) {
            final int diff = Misc.signedToUnsignedByte(b1[i])
                - Misc.signedToUnsignedByte(b2[i]);
            if (diff < 0) {
                return -1;
            } else if (diff > 0) {
                return 1;
            } else {
                return 0;
            } // end if/elsif/else
        } else {
            if (len1 < len2) {
                return -1;
            } else if (len1 > len2) {
                return 1;
            } else {
                return 0;
            } // end if/elsif/else
        } // end if
    } // end method 'compare(Value, Value)'

} // end class 'Str'
